package xxl.ioc.reflection 
{
	import avmplus.DescribeTypeJSON;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	import xxl.ioc.typedescriptions.ConstructorInjectionPoint;
	import xxl.ioc.typedescriptions.MethodInjectionPoint;
	import xxl.ioc.typedescriptions.NoParamsConstructorInjectionPoint;
	import xxl.ioc.typedescriptions.PostConstructInjectionPoint;
	import xxl.ioc.typedescriptions.PreDestroyInjectionPoint;
	import xxl.ioc.typedescriptions.PropertyInjectionPoint;
	import xxl.ioc.typedescriptions.TypeDescription;
	/**
	 * ...
	 * @author XXL
	 */
	public class DescribeTypeJSONReflector extends ReflectorBase implements IReflector 
	{
		private const _descriptor:DescribeTypeJSON = new DescribeTypeJSON();
		
		public function DescribeTypeJSONReflector() 
		{
			
		}
		
		/* INTERFACE xxl.ioc.reflection.IReflector */
		
		public function typeImplements(type:Class, superType:Class):Boolean 
		{
			if (type == superType)
			{
				return true;
			}
			const superClassName:String = getQualifiedClassName(superType);
			const traits:Object = _descriptor.getInstanceDescription(type).traits;
			return (traits.bases as Array).indexOf(superClassName) > -1 
				|| (traits.interfaces as Array).indexOf(superClassName) > -1;
		}
		
		public function describeInjections(type:Class):TypeDescription 
		{
			const rawDescription:Object = _descriptor.getInstanceDescription(type);
			const traits:Object = rawDescription.traits;
			const typeName:String = rawDescription.name;
			const description:TypeDescription = new TypeDescription(false);
			addCtorInjectionPoint(description, traits, typeName);
			addFieldInjectionPoints(description, traits.variables);
			addFieldInjectionPoints(description, traits.accessors);
			addMethodInjectionPoints(description, traits.methods, typeName);
			addPostConstructMethodPoints(description, traits.variables, typeName);
			addPostConstructMethodPoints(description, traits.accessors, typeName);
			addPostConstructMethodPoints(description, traits.methods, typeName);
			addPreDestroyMethodPoints(description, traits.methods, typeName);
			return description;
		}
		
		//----------private
		
		private function addCtorInjectionPoint(description:TypeDescription, traits:Object, typeName:String):void
		{
			const parameters:Array = traits.constructor;
			if (!parameters)
			{
				description.ctor = traits.bases.length > 0
					? new NoParamsConstructorInjectionPoint()
					: null;
				return;
			}
			const injectParameters:Dictionary = extractTagParameters("Inject"
				, traits.metadata);
			const parameterNames:Array = (injectParameters && injectParameters.name || "").split(",");
			const requiredParameters:int = gatherMethodParameters(parameters, parameterNames, typeName);
			description.ctor = new ConstructorInjectionPoint(parameters, requiredParameters, injectParameters);
		}
		
		private function addFieldInjectionPoints(description:TypeDescription, fields:Array):void
		{
			if (!fields)
			{
				return;
			}
			const len:int = fields.length;
			for (var i:int = 0; i < len; i++) 
			{
				const field:Object = fields[i];
				const injectParameters:Dictionary = extractTagParameters("Inject"
					, field.metadata);
				if (!injectParameters)
				{
					continue;
				}
				const mappingName:String = injectParameters.name || "";
				const optional:Boolean = injectParameters.optional == "true";
				const injectionPoint:PropertyInjectionPoint = new PropertyInjectionPoint(field.type + "|" + mappingName
					, field.name
					, optional
					, injectParameters);
				description.addInjectionPoint(injectionPoint);
			}
		}
		
		private function addMethodInjectionPoints(description:TypeDescription, methods:Array, typeName:String):void
		{
			if (!methods)
			{
				return;
			}
			const len:int = methods.length;
			for (var i:int = 0; i < len; i++) 
			{
				const method:Object = methods[i];
				const injectParameters:Dictionary = extractTagParameters("Inject"
					, method.metadata);
				if (!injectParameters)
				{
					continue;
				}
				const optional:Boolean = injectParameters.optional == "true";
				const parameterNames:Array = (injectParameters.name || "").split(",");
				const parameters:Array = method.parameters;
				const requiredParameters:uint = gatherMethodParameters(parameters, parameterNames, typeName);
				const injectionPoint:MethodInjectionPoint = new MethodInjectionPoint(method.name
					, parameters
					, requiredParameters
					, optional
					, injectParameters);
				description.addInjectionPoint(injectionPoint);
			}
		}
		
		private function addPostConstructMethodPoints(description:TypeDescription, methods:Array, typeName:String):void
		{
			const injectionPoints:Array = gatherOrderedInjectionPointsForTag(PostConstructInjectionPoint
				, "PostConstruct"
				, methods
				, typeName);
			const len:int = injectionPoints.length;
			for (var i:int = 0; i < len; i++) 
			{
				description.addInjectionPoint(injectionPoints[i]);
			}
		}
		
		private function addPreDestroyMethodPoints(description:TypeDescription, methods:Array, typeName:String):void
		{
			const injectionPoints:Array = gatherOrderedInjectionPointsForTag(PreDestroyInjectionPoint
				, "PreDestroy"
				, methods
				, typeName);
			if (!injectionPoints.length)
			{
				return;
			}
			description.preDestoryMethods = injectionPoints[0];
			description.preDestoryMethods.last = injectionPoints[0];
			const len:int = injectionPoints.length;
			for (var i:int = 1; i < len; i++)
			{
				description.preDestoryMethods.last.next = injectionPoints[i];
				description.preDestoryMethods.last = injectionPoints[i];
			}
		}
		
		private function extractTagParameters(tag:String, metadata:Array):Dictionary
		{
			const len:int = metadata?metadata.length:0;
			for (var i:int = 0; i < len; i++)
			{
				const entry:Object = metadata[i];
				if (entry.name == tag)
				{
					const parametersList:Array = entry.value;
					const parametersMap:Dictionary = new Dictionary();
					const parametersCount:int = parametersList.length;
					for (var j:int = 0; j < parametersCount; j++) 
					{
						const parameter:Object = parametersList[j];
						parametersMap[parameter.key] = parametersMap[parameter.key]
							? parametersMap[parameter.key] + "," + parameter.value
							: parameter.value;
					}
					return parametersMap;
				}
			}
			return null;
		}
		
		private function gatherMethodParameters(parameters:Array, parameterNames:Array, typeName:String):int
		{
			var requiredLength:int = 0;
			const len:int = parameters.length;
			for (var i:int = 0; i < len; i++) 
			{
				const parameter:Object = parameters[i];
				const injectionName:String = parameterNames[i] || "";
				var parameterTypeName:String = parameter.type;
				if (parameterTypeName == "*")
				{
					if (!parameter.optional)
					{
						throw new Error("定义的方法参数类型不能为*");
					}
					else
					{
						parameterTypeName = null;
					}
				}
				if (!parameter.optional)
				{
					requiredLength++;
				}
				parameters[i] = parameterTypeName + "|" + injectionName;
			}
			return requiredLength;
		}
		
		private function gatherOrderedInjectionPointsForTag(injectionPointClass:Class, tag:String, methods:Array, typeName:String):Array
		{
			const injectionPoints:Array = [];
			if (!methods)
			{
				return injectionPoints;
			}
			const len:int = methods.length;
			for (var i:int = 0; i < len; i++) 
			{
				const method:Object = methods[i];
				const injectParameters:Object = extractTagParameters(tag, method.metadata);
				if (!injectParameters)
				{
					continue;
				}
				const parameterNames:Array = (injectParameters.name || "").split(",");
				var parameters:Array = method.parameters;
				var requiredParameters:int;
				if (parameters)
				{
					requiredParameters = gatherMethodParameters(parameters, parameterNames, typeName);
				}
				else
				{
					parameters = [];
					requiredParameters = 0;
				}
				var order:int = parseInt(injectParameters.order, 10);
				if (order.toString(10) != injectParameters.order)
				{
					order = int.MAX_VALUE;
				}
				injectionPoints.push(new injectionPointClass(method.name, parameters, requiredParameters, order));
			}
			if (injectionPoints.length > 0)
			{
				injectionPoints.sortOn("order", Array.NUMERIC);
			}
			return injectionPoints;
		}
	}

}